package org.rowan.adi.components.comp2d.polygon;

import java.util.LinkedList;
import org.rowan.adi.Vector;
import org.rowan.adi.components.comp2d.LineSegment2D;
import org.rowan.adi.components.comp2d.Point2D;
import org.rowan.adi.linearalgebra.LinearAlgebra2D;

/**
 * <p>The IrregularConvexPolygon class provides functionality for an irregular polygon in
 * two dimensional space. A polygon is said to be convex if all of its interior
 * angles are less than or equal to 180 degress. A polygon is said to be irregular
 * if the length of edges are not equal. It is important the list of points provided
 * are in clockwise order and do not violate the rules for being a convex polygon.
 * Failure to comply to these rules will result in an exception being thrown.</p>
 *
 * @author Spence DiNicolantino, Robert Russell
 * @version 1.0
 */
public class IrregularConvexPolygon extends Polygon {

    /**
     * Constructs an instance of IrregularPolygon.
     * @param points The list of 2D Points, in clockwise order, that represent
     * this Irregular Polygon.
     * @throws IllegalArgumentException If the points provided violate the rules for
     * creating a convex polygon.
     */
    public IrregularConvexPolygon(LinkedList<Point2D> points) {
        if (points.size() <= 2) {
            throw new IllegalArgumentException("This is not a polygon. Please provide three or more points.");
        } else if (validatePoints(points)) {
            super.setListofPoints(points);
            Rectangle rectangle = this.getBoundingBox();
            super.setLocation(rectangle.getLocation());
        } else {
            throw new IllegalArgumentException("The points provided violate the " +
                    "rules for creating a convex polygon");
        }
    }

    /**
     * Determines whether the given set of points will create a valid convex polygon,
     * with clockwise winding.
     * @param points A List containing the points, in clockwise fashion, of the
     * proposed convex polygon.
     * @return <code>true</code> if the given set of points will create a valid
     * convex polygon; <code>false</code> otherwise.
     * @throws IllegalArgumentException the list of points is empty.
     */
    protected boolean validatePoints(LinkedList<Point2D> points) {
        if (points.size() == 0) {
            throw new IllegalArgumentException("The list of points is empty.");
        }

        int pointCount = points.size();
        for (int i = 0; i < pointCount; i++) {
            Point2D a = points.get(i);
            Point2D b = points.get((i + 1) % pointCount);
            Point2D c = points.get((i + 2) % pointCount);

            Vector v1 = b.getLocation().subtract(a.getLocation());
            Vector v2 = c.getLocation().subtract(b.getLocation());

            double crossMag = (v1.getX() * v2.getY()) - (v1.getY() * v2.getX());
            if (crossMag > 0.0) {
                return false;
            }
        }

        return true;
    }

    /**
     * Computes the area of the Irregular Polygon by using triangulation.
     * @return the area of the Irregular Polygon.
     */
    public double computeArea() {
        //Local copy of the points since points will be removed later in the method
        LinkedList<Point2D> points = new LinkedList<Point2D>();
        for (Point2D point : super.getListofPoints()) {
            points.add(new Point2D(point.getLocation()));
        }

        double area = 0;
        int pointIndex = 0;
        boolean quad = (points.size() == 4);

        // Continue to triangulate until there are three points left, which means
        // the final triangle can be assembled and computed.
        while (points.size() > 3) {

            // Start with a point and attempt make a line segment with the point
            // two points in front of this point when going clockwise.
            Point2D point = points.get(pointIndex);
            Point2D connection = points.get((Math.abs(pointIndex + 2)) % points.size());
            LineSegment2D segment = new LineSegment2D(point, connection);

            // If the line segment is within the polygon, that means it is a valid
            // connection. Since we went two points ahead, a triangle has been
            // made with the edge between point and the point skipped, the line
            // segment just created, and the edge between the point skipped and
            // the connection point. Compute the area of the triangle and remove
            // the skipped point.
            if (LinearAlgebra2D.contains(this, segment)) {
                Point2D middle = points.get((pointIndex + 1) % points.size());
                area += computeTriangulation(point, middle, connection);
                points.remove(middle);
            }

            // Using the same point, attempt make a line segment with the point
            // two points in back of this of this point when going clockwise.
            connection = points.get((Math.abs(pointIndex - 2)) % points.size());
            segment = new LineSegment2D(point, connection);

            // If the line segment is within the polygon, that means it is a valid
            // connection. Since we went two points behind, a triangle has been
            // made with the edge between point and the point skipped, the line
            // segment just created, and the edge between the point skipped and
            // the connection point. Compute the area of the triangle and remove
            // the skipped point.
            if (LinearAlgebra2D.contains(this, segment)) {
                Point2D middle = points.get((pointIndex + 1) % points.size());
                area += computeTriangulation(point, middle, connection);
                points.remove(middle);
            }

            // Move onto the next available point in the remaining polygon.
            pointIndex++;

            if (pointIndex == points.size()) {
                pointIndex = 0;
            }
        }

        // Compute the area of the last triangle since three points remain.
        // Quads remove two points one time through the loop and complete their calculation
        // so this is not needed for them.
        if (!quad) {
            area += computeTriangulation(points.get(0), points.get(1), points.get(2));
        }
        return area;
    }

    /**
     * Computes the area of the triangle based on the three 2D Points provided.
     * Heron's formula is used to compute the area, as only the sides are known.
     * @param point1 the first 2D Point of the triangle.
     * @param point2 the second 2D Point of the triangle.
     * @param point3 the third 2D Point of the triangle.
     * @return the area of the triangle based on the three 2D Points provided.
     */
    private double computeTriangulation(Point2D point1, Point2D point2, Point2D point3) {
        double a = Math.sqrt(Math.pow(point2.getX() - point1.getX(), 2) + Math.pow(point2.getY() - point1.getY(), 2));
        double b = Math.sqrt(Math.pow(point3.getX() - point2.getX(), 2) + Math.pow(point3.getY() - point2.getY(), 2));
        double c = Math.sqrt(Math.pow(point3.getX() - point1.getX(), 2) + Math.pow(point3.getY() - point1.getY(), 2));
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }
}
